﻿Option Strict On

Namespace Translation

  ''' <summary>
  ''' Represents a Braille translator.
  ''' </summary>
  ''' <remarks>This class cannot be inherited.</remarks>
  Public NotInheritable Class Translator

    ''' <summary>
    ''' The language configuration object.
    ''' </summary>
    ''' <remarks></remarks>
    Private Langauges As LanguageConfiguration

    ''' <summary>
    ''' The classified maps of a single character from the language definition.
    ''' </summary>
    ''' <remarks></remarks>
    Private SingletonMaps As SortedDictionary(Of String, SortedDictionary(Of Char, String))

    ''' <summary>
    ''' The unified map of every single character from the language definition.
    ''' </summary>
    ''' <remarks></remarks>
    Private UniversalMap As SortedDictionary(Of Char, String)

    ''' <summary>
    ''' The root of lexical tree structure.
    ''' </summary>
    ''' <remarks></remarks>
    Private SyntacticTree As Node

    ''' <summary>
    ''' A stub of English capital letter indicator.
    ''' </summary>
    ''' <remarks>The final code of this attribute is comma sign (<code>,</code>).</remarks>
    Private CapitalLetterIndicatorStub As Char

    ''' <summary>
    ''' A stub of Arabic number indicator.
    ''' </summary>
    ''' <remarks>The final code of this attribute is number sign (<code>#</code>).</remarks>
    Private ArabicNumberIndicatorStub As Char

    ''' <summary>
    ''' A stub of Thai number indicator.
    ''' </summary>
    ''' <remarks>The final code of this attribute is comma followed by number sign (<code>,#</code>).</remarks>
    Private ThaiNumberIndicatorStub As Char

    ''' <summary>
    ''' Initializes a Braille translator with specified language definition and configuration.
    ''' </summary>
    ''' <param name="FilePath">The XML language definition file path.</param>
    ''' <param name="Languages">The language configuration object.</param>
    ''' <exception cref="System.FormatException">Invalid lexical format.</exception>
    ''' <remarks></remarks>
    Public Sub New(ByVal FilePath As String, ByVal Languages As LanguageConfiguration)
      'Validates file path.
      If String.IsNullOrEmpty(FilePath) Then
        Throw New ArgumentNullException("FilePath", "FilePath cannot be null.")
      ElseIf Not System.IO.File.Exists(FilePath) Then
        Throw New System.IO.FileNotFoundException("Specificed file path does not exist.", FilePath)
      End If

      'Assigns a language configuration object.
      MyClass.Langauges = Languages

      'Creates attributes.
      SingletonMaps = New SortedDictionary(Of String, SortedDictionary(Of Char, String))
      SingletonMaps.Add("", New SortedDictionary(Of Char, String)) 'default map
      UniversalMap = New SortedDictionary(Of Char, String)
      SyntacticTree = New Node(Nothing, Nothing)

      'Reads each tag in XML langauge definition file.
      Dim Reader As New System.Xml.XmlTextReader(FilePath)
      While Reader.Read
        Select Case Reader.NodeType
          Case Xml.XmlNodeType.Element
            'Found culture tag.
            If Reader.Name.Equals("culture") Then
              'Reads culture name.
              Dim CultureName As String = Reader.GetAttribute("name")
              'Checks whether language configuration is accepts current culture.
              If String.IsNullOrEmpty(CultureName) OrElse Languages Is Nothing OrElse Languages.ContainsCulture(CultureName) Then
                'Reads each inner-tag in culture tag.
                While Reader.Read
                  If Reader.NodeType = Xml.XmlNodeType.Element Then
                    'Found a singleton tag.
                    If Reader.Name.StartsWith("singleton") Then
                      'Reads a regular list of current singleton (if any).
                      Dim ClassList As String = Reader.GetAttribute("class")
                      Dim CurrentMaps As New List(Of SortedDictionary(Of Char, String))(3)
                      If String.IsNullOrEmpty(ClassList) Then
                        'Regular list does not exists, prepares a default map.
                        CurrentMaps.Add(SingletonMaps.Item(""))
                      Else
                        'Found one or more regular map.
                        Dim MapNames As String() = ClassList.Split(",;".ToCharArray)
                        For i As Integer = 0 To MapNames.Length - 1
                          If SingletonMaps.ContainsKey(MapNames(i)) Then
                            'Found an existing map, fetches it.
                            CurrentMaps.Add(SingletonMaps.Item(MapNames(i).Trim))
                          Else 'Not found a specified map name, creates a new map and adds it to singleton maps.
                            CurrentMaps.Add(New SortedDictionary(Of Char, String))
                            SingletonMaps.Add(MapNames(i).Trim, CurrentMaps.Last)
                          End If
                        Next
                      End If

                      'Reads key and value of current singleton.
                      Dim Key As String = Reader.GetAttribute("key")
                      Dim Code As String = Reader.GetAttribute("code")
                      'Checks whether key and value are nothing or empty.
                      If String.IsNullOrEmpty(Key) OrElse String.IsNullOrEmpty(Code) Then
                        Throw New FormatException("Invalid syntax at line " & Reader.LineNumber & ", position " & Reader.LinePosition)
                      Else
                        'Escapes from XML character.
                        Key = EscapeXMLCharacter(Key)
                        'Checks whether singleton key has exactly one character.
                        If Key.Length <> 1 Then
                          Throw New FormatException
                        End If
                        'Escapes from XML character.
                        Code = EscapeXMLCharacter(Code)
                        'Adds current code to the related map(s).
                        For i As Integer = 0 To CurrentMaps.Count - 1
                          CurrentMaps(i).Add(Key.Chars(0), Code)
                        Next
                        'Adds current code to the universal map.
                        UniversalMap.Add(Key.Chars(0), Code)
                      End If

                      'Found a composite tag.
                    ElseIf Reader.Name.Equals("composite") Then
                      'Reads key and value of current composite.
                      Dim Key As String = Reader.GetAttribute("key")
                      Dim Code As String = Reader.GetAttribute("code")
                      'Checks whether key and value are nothing or empty.
                      If String.IsNullOrEmpty(Key) OrElse String.IsNullOrEmpty(Code) Then
                        Throw New FormatException("Invalid syntax at line " & Reader.LineNumber & ", position " & Reader.LinePosition)
                      Else
                        'Escapes from XML character.
                        Key = EscapeXMLCharacter(Key)
                        Code = EscapeXMLCharacter(Code)
                        'Prepares a root of syntactic tree.
                        Dim CurrentNode As Node = SyntacticTree

                        'For each character in the current code sequence.
                        For i As Integer = 0 To Key.Length - 1
                          'Checks whether current character is regular.
                          'A regular character is a combination of [REGULAR:IDENTIFIER], where REGULAR is a regular map name.
                          If Key.Chars(i) = "[" AndAlso Key.IndexOf("]", i) > 0 Then
                            Dim CloseBracketIndex As Integer = Key.IndexOf("]", i)
                            'Checks whether a sequence of code has a close bracket.
                            If CloseBracketIndex = -1 Then
                              'Found an open bracket but cannot find a close bracket, throw an exception.
                              Throw New FormatException("Invalid syntax at line " & Reader.LineNumber & ", position " & Reader.LinePosition)
                            Else
                              'Checks whether current character is a rightmost character of the current code sequence.
                              If CloseBracketIndex = Key.Length - 1 Then
                                'Adds a current node to be a child of previous node.
                                'Note that a node which to be added is an accepting node.
                                CurrentNode = CurrentNode.AddChild(New Node(Key.Substring(i, CloseBracketIndex - i + 1), Code, SingletonMaps))
                              Else
                                'Adds a current node to be a child of previous node.
                                'Note that a node which to be added is ***NOT*** an accepting node.
                                CurrentNode = CurrentNode.AddChild(New Node(Key.Substring(i, CloseBracketIndex - i + 1), Nothing, SingletonMaps))
                              End If
                              'Moves index to the next proper character.
                              i = CloseBracketIndex
                            End If

                          ElseIf i = Key.Length - 1 Then
                            'Current character is not regular, but it is the rightmost character of code sequence.
                            'Adds a current node to be a child of previous node.
                            'Note that a node which to be added is an accepting node.
                            CurrentNode = CurrentNode.AddChild(New Node(Key.Chars(Key.Length - 1), Code))

                          Else
                            'Current character is not regular, and it is not the rightmost character of code sequence.
                            'Adds a current node to be a child of previous node.
                            'Note that a node which to be added is ***NOT*** an accepting node.
                            CurrentNode = CurrentNode.AddChild(New Node(Key.Chars(i), Nothing))
                          End If
                          'Moves index to the next character of code sequence.
                        Next
                      End If

                      'Found an extension tag.
                      'Checks whether language configuration is not allows current extionsion.
                    ElseIf Reader.Name.Equals("extension") AndAlso Not Languages.ContainsExtension(CultureName, Reader.GetAttribute("name")) Then
                      'Skips all inner-tag of current extension.
                      Do
                      Loop Until Reader.Read AndAlso Reader.NodeType = Xml.XmlNodeType.EndElement AndAlso Reader.Name.Equals("extension")
                    End If

                    'Found an end tag of culture.
                  ElseIf Reader.NodeType = Xml.XmlNodeType.EndElement AndAlso Reader.Name.Equals("culture") Then
                    'Exits current culture and fetches the next culture (if any).
                    Exit While
                  End If

                End While
              End If
            End If

        End Select
      End While
      'Terminates an XML reader.
      Reader.Close()

      'Finds and assigns a value for each stub.
      Dim UnicodePoint As Integer = AscW("A")
      Do While UniversalMap.ContainsKey(Chr(UnicodePoint))
        UnicodePoint += 1
      Loop
      CapitalLetterIndicatorStub = Chr(UnicodePoint)
      UniversalMap.Add(CapitalLetterIndicatorStub, ",")

      Do While UniversalMap.ContainsKey(Chr(UnicodePoint))
        UnicodePoint += 1
      Loop
      ArabicNumberIndicatorStub = Chr(UnicodePoint)
      UniversalMap.Add(ArabicNumberIndicatorStub, "#")

      Do While UniversalMap.ContainsKey(Chr(UnicodePoint))
        UnicodePoint += 1
      Loop
      ThaiNumberIndicatorStub = Chr(UnicodePoint)
      UniversalMap.Add(ThaiNumberIndicatorStub, ",#")
    End Sub

    ''' <summary>
    ''' Translates text to Braille code using the current langauge definition.
    ''' </summary>
    ''' <param name="Text"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Translate(ByVal Text As String) As String
      'Creates a StringBuilder object from the input text.
      Dim Output As New System.Text.StringBuilder(Text)

      'Inserts indicators.
      Output = InsertIndicator(Output)

      'Coverts text to Braille code.
      Output = CovertToBraille(Output)

      'Returns translated Brialle code.
      Return Output.ToString
    End Function

    ''' <summary>
    ''' Inserts one or more indicators into text input text.
    ''' </summary>
    ''' <param name="Text">The input text.</param>
    ''' <returns>The input text with one or more indicators.</returns>
    ''' <remarks>There are only 3 indicators available:
    ''' 1) Arabic number indicator
    ''' 2) English capital letter indicator
    ''' 3) Thai number indicator</remarks>
    Private Function InsertIndicator(ByRef Text As System.Text.StringBuilder) As System.Text.StringBuilder
      'Initializes an indexer.
      Dim CurrentIndex As Integer = 0
      'Iterates on input text character.
      Do Until CurrentIndex = Text.Length
        'Inserts Arabic number indicator.
        If Char.IsDigit(Text.Chars(CurrentIndex)) AndAlso
          (CurrentIndex = 0 OrElse Not (Char.IsDigit(Text.Chars(CurrentIndex - 1)) OrElse Text.Chars(CurrentIndex - 1) = ",")) Then
          Text.Insert(CurrentIndex, ArabicNumberIndicatorStub)
          CurrentIndex += 1
          Do
            CurrentIndex += 1
          Loop While CurrentIndex < Text.Length AndAlso (Char.IsDigit(Text.Chars(CurrentIndex)) OrElse Text.Chars(CurrentIndex - 1) = ",")
          CurrentIndex -= 1

          'Inserts English capital letter indicators.
        ElseIf Char.IsUpper(Text.Chars(CurrentIndex)) Then
          'Gets a last letter index of the current word.
          Dim LastWordLetterIndex As Integer = CurrentIndex
          For NextIndex As Integer = CurrentIndex + 1 To Text.Length - 1
            LastWordLetterIndex = NextIndex
            If Not Char.IsLetter(Text.Chars(NextIndex)) Then
              LastWordLetterIndex = NextIndex - 1
              Exit For
            End If
          Next

          'Checks whether the current word is entire upper case.
          Dim IsUpperEntireWord As Boolean = True
          For LetterIndex As Integer = CurrentIndex To LastWordLetterIndex
            IsUpperEntireWord = IsUpperEntireWord And Char.IsUpper(Text.Chars(LetterIndex))
          Next

          'Indicates ",," for the entire upper case word.
          If IsUpperEntireWord And CurrentIndex <> LastWordLetterIndex Then
            For LetterIndex As Integer = LastWordLetterIndex To CurrentIndex Step -1
              Text.Chars(LetterIndex) = Char.ToLower(Text.Chars(LetterIndex))
            Next
            Text.Insert(CurrentIndex, CapitalLetterIndicatorStub & CapitalLetterIndicatorStub)
            CurrentIndex += (2 + LastWordLetterIndex - CurrentIndex)

          Else 'Indicates "," for each upper case character.
            Dim IndicatorCount As Integer = 0
            For LetterIndex As Integer = LastWordLetterIndex To CurrentIndex Step -1
              If Char.IsUpper(Text.Chars(LetterIndex)) Then
                Text.Chars(LetterIndex) = Char.ToLower(Text.Chars(LetterIndex))
                Text.Insert(LetterIndex, CapitalLetterIndicatorStub)
                IndicatorCount += 1
              End If
            Next
            CurrentIndex += (IndicatorCount + LastWordLetterIndex - CurrentIndex)
          End If

          'Inserts Thai number indicators.
        ElseIf IsThaiDigit(Text.Chars(CurrentIndex)) AndAlso
          (CurrentIndex = 0 OrElse Not (IsThaiDigit(Text.Chars(CurrentIndex - 1)) OrElse Text.Chars(CurrentIndex - 1) = ",")) Then
          Text.Insert(CurrentIndex, ThaiNumberIndicatorStub)
          CurrentIndex += 1
          Do
            CurrentIndex += 1
          Loop While CurrentIndex < Text.Length AndAlso (IsThaiDigit(Text.Chars(CurrentIndex)) OrElse Text.Chars(CurrentIndex - 1) = ",")
          CurrentIndex -= 1

        End If
        'Move to the next character.
        CurrentIndex += 1
      Loop

      'Returns the input text with one or more indicators.
      Return Text
    End Function

    ''' <summary>
    ''' Converts the input text to Braille code character by character.
    ''' </summary>
    ''' <param name="Text">The sequence of Braille code.</param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function CovertToBraille(ByRef Text As System.Text.StringBuilder) As System.Text.StringBuilder
      'Gets a last character index of the input text.
      Dim LastIndex As Integer = Text.Length - 1

      'Creates an output buffer.
      Dim Output As New System.Text.StringBuilder(LastIndex)

      'Creates a node reference.
      Dim w As Node = Nothing

      'For each character in the input text.
      For CurrentIndex As Integer = 0 To LastIndex
        'Checks whether the current character is accepted by the root of syntactic tree.
        If SyntacticTree.ChildAccepts(Text.Chars(CurrentIndex), w) Then
          Dim NextIndex As Integer = CurrentIndex + 1
          Do
            'Finds the longest sequence of character that is accepted by a syntactic tree.
            If NextIndex > LastIndex OrElse Not w.ChildAccepts(Text.Chars(NextIndex), w) Then
              'If the longest node is an accepting node.
              If w.IsHaltAccept Then
                'Traces a path value.
                Dim x As System.Text.StringBuilder = Nothing
                Do
                  x = w.GetValue(x)
                  w.ResetState()
                  w = w.Parent
                Loop Until w.IsRoot
                w.ResetState()
                'Appends a path value to the output.
                Output.Append(x.ToString)
                'Continues with the next character.
                CurrentIndex = NextIndex - 1
                Exit Do

              Else 'If the longest node is not an accepting node
                'Back tracks
                Do
                  w.ResetState()
                  w = w.Parent
                  NextIndex -= 1
                  If w.ChildAccepts(Text.Chars(NextIndex), w) Then
                    'Found an accepting node, tries to get a value.
                    GoTo NextIndex
                  ElseIf w.IsRoot Then
                    'Not found any accepting node anymore, parses an input character using singleton maps.
                    w.ResetState()
                    GoTo SingletonTranslation
                  End If
                Loop
              End If
            End If
NextIndex:  'Move to the next character.
            NextIndex += 1
          Loop

        Else 'Parses a current input character using singleton maps.
SingletonTranslation:
          Dim Buffer As String = Nothing
          'Tries to get a code value from the universal map.
          If UniversalMap.TryGetValue(Text.Chars(CurrentIndex), Buffer) Then
            Output.Append(Buffer)
          Else 'An input character does not exists in the universal map.
            Output.Append(Text.Chars(CurrentIndex))
          End If
        End If
      Next

      'Returns a sequence of code.
      Return Output
    End Function

    ''' <summary>
    ''' Finds and replaces XML escape characters.
    ''' </summary>
    ''' <param name="Text">The text that may contains one or more XML escape characters.</param>
    ''' <returns>The parsed input text.</returns>
    ''' <remarks></remarks>
    Private Shared Function EscapeXMLCharacter(ByVal Text As String) As String
      If Text.Contains("!") Then
        Text = Text.Replace("!ampersand", "&")
        Text = Text.Replace("!lessthan", "<")
        Text = Text.Replace("!doublequote", Chr(34))
      End If
      Return Text
    End Function

    ''' <summary>
    ''' Determines whether an input character is Thai number.
    ''' </summary>
    ''' <param name="Character">An input character.</param>
    ''' <returns>True if an input character is Thai number; otherwise, false.</returns>
    ''' <remarks></remarks>
    Private Shared Function IsThaiDigit(ByVal Character As Char) As Boolean
      Dim UnicodePoint As Integer = AscW(Character)
      Return UnicodePoint >= 3664 AndAlso UnicodePoint <= 3673
    End Function

  End Class
End Namespace